﻿using GDM.Hogar.Soporte.Atributos;
using GDM.Hogar.Soporte.Helpers;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Windows.Forms;

namespace GDM.Hogar.Controles.ControlesPersonalizados.Grilla
{
    public static class SupportPagingExtensions
    {
        #region Public Methods

        public static void Configure<T>(this ISupportPaging grid, IQueryable<T> query, int pageSize = 100)
        {
            ValidateInput(grid, query, pageSize);

            grid.PageSize = pageSize;
            grid.TotalPages = null;
            PagingCache.Invalidate();
            ConfigureInitialGridActions(grid, query);

            if (grid.EndConfigure != null)
                grid.EndConfigure();
            grid.Configured = true;
        }

        public static void Search<T>(this ISupportPaging grid,
            IQueryable<T> source,
            Func<T, bool> criteria)
        {
            if (!grid.Configured)
                throw new InvalidOperationException(
                    "You have to configure the grid first. Take a look at the configure method");

            PagingCache.Invalidate();
            var query = source.Where(criteria).AsQueryable();

            grid.TotalPages = null;
            ConfigureInitialGridActions(grid, query);
        }

        #endregion Public Methods

        #region Private Methods

        private static int CalculatePages(ISupportPaging grid, int total)
        {
            if (total == 0) return 1;

            var result = total / grid.PageSize;
            if (total % grid.PageSize != 0)
                result++;
            return result;
        }

        private static void ConfigureInitialGridActions<T>(ISupportPaging grid, IQueryable<T> query)
        {
            grid.OnGotoFirst = () => grid.SetDataSource(First, query);
            grid.OnGotoPrev = () => grid.SetDataSource(Prev, query);
            grid.OnGotoNext = () => grid.SetDataSource(Next, query);
            grid.OnGotoLast = () => grid.SetDataSource(Last, query);

            if (grid.TotalPages == null)
            {
                //local copy to avoid possible multiple enumerations.
                //var total = totalRowCount();
                var total = query.Count();
                grid.TotalRecords = () => total;
                //
                grid.TotalPages = () => CalculatePages(grid, total);
            }

            //Goes to the first page.
            grid.SetDataSource(First, query);
            grid.CleanUp = () => PagingCache.Remove(grid); //<= to avoid memory  leaks.
        }

        //paging strategies.
        private static IEnumerable<T> First<T>(this ISupportPaging grid,
            IQueryable<T> dataSource)
        {
            grid.CurrentPage = 0;
            return grid.GetPage(dataSource);
        }

        private static void GenerateColumns<T>(this ISupportPaging grid)
        {
            var entity = Activator.CreateInstance<T>();
            var etype = entity.GetType();
            IEnumerable<PropertyInfo> pis = etype.GetProperties(BindingFlags.Instance | BindingFlags.Public);
            foreach (var pi in pis)
            {
                var dnAttr = pi.GetFirstAttribute<DisplayNameAttribute>();
                var dAttr = pi.GetFirstAttribute<MostrarAttribute>();
                var cWidthAttr = pi.GetFirstAttribute<AnchoAttribute>();
                var cIndexAttr = pi.GetFirstAttribute<IndiceAttribute>();
                var column = new DataGridViewColumn
                    {
                        CellTemplate = new DataGridViewTextBoxCell(),
                        DataPropertyName = pi.Name,
                        Name = pi.Name,
                        HeaderText = dnAttr == null ? pi.Name : dnAttr.DisplayName,
                        AutoSizeMode = DataGridViewAutoSizeColumnMode.DisplayedCells
                    };
                if (pi.PropertyType == (typeof(DateTime)) || pi.PropertyType == (typeof(DateTime?)))
                {
                    column.DefaultCellStyle.Format = "dd/MM/yyyy";
                }
                if (cWidthAttr != null)
                {
                    column.MinimumWidth = cWidthAttr.Ancho;
                }
                if (cIndexAttr != null)
                {
                    column.DisplayIndex = cIndexAttr.Indice;
                }

                grid.Columns.Add(column);
                column.Visible = dAttr == null || dAttr.Mostrar;
            }
        }

        private static IEnumerable<T> GetPage<T>(this ISupportPaging grid,
            IQueryable<T> dataSource)
        {
            return PagingCache.CacheAndReturn(grid, dataSource);
        }

        private static IEnumerable<T> Last<T>(this ISupportPaging grid, IQueryable<T> dataSource)
        {
            if (grid.TotalPages() > 0)
                grid.CurrentPage = grid.TotalPages() - 1;
            return grid.GetPage(dataSource);
        }

        private static IEnumerable<T> Next<T>(this ISupportPaging grid, IQueryable<T> dataSource)
        {
            if (grid.CurrentPage < grid.TotalPages() - 1)
                grid.CurrentPage++;
            return grid.GetPage(dataSource);
        }

        //private static SortableBindingList<T> QueryToSortableDataSource<T>(IEnumerable<T> queryResult)
        //{
        //    return new SortableBindingList<T>(queryResult);
        //}
        private static IEnumerable<T> Prev<T>(this ISupportPaging grid, IQueryable<T> dataSource)
        {
            if (grid.CurrentPage != 0)
                grid.CurrentPage--;
            return grid.GetPage(dataSource);
        }

        private static void SetDataSource<T>(this ISupportPaging grid, Func<ISupportPaging, IQueryable<T>, IEnumerable<T>> gotoPage, IQueryable<T> source)
        {
            grid.DataSource = null;
            grid.Columns.Clear();

            //- TODO Add Sorting for all pages
            grid.DataSource = gotoPage(grid, source); //QueryToSortableDataSource(gotoPage(grid, source));

            if (source.Any())
                grid.GenerateColumns<T>();

            if (grid.OnPageChange != null)
                grid.OnPageChange();
        }

        private static void ValidateInput<T>(ISupportPaging grid, IQueryable<T> query, int pageSize)
        {
            if (grid == null)
                throw new ArgumentNullException("grid");

            if (query == null)
                throw new ArgumentNullException("query");

            if (pageSize < 1)
                throw new ArgumentException("Page size must be greater than zero.");
        }

        #endregion Private Methods

        #region Caching

        /// <summary>
        /// This class takes care of datasource caching.
        /// </summary>
        private static class PagingCache
        {
            private static readonly Dictionary<CacheKey, object> Storage;

            static PagingCache()
            {
                Storage = new Dictionary<CacheKey, object>();
            }

            public static IEnumerable<T> CacheAndReturn<T>(ISupportPaging grid,
                IQueryable<T> dataSource)
            {
                var result = GetCached<T>(grid);
                if (result == null)
                    Cache(grid,
                        //paging in action
                          (from p in dataSource select p)
                              .Skip( /*start page*/grid.CurrentPage * grid.PageSize)
                              .Take(grid.PageSize)
                              .ToList()); //only at this point we hit the database
                //***********************************************************
                return GetCached<T>(grid);
            }

            public static void Invalidate()
            {
                Storage.Clear();
            }

            internal static void Remove(ISupportPaging grid)
            {
                var key = new CacheKey(grid);
                if (Storage.ContainsKey(key))
                    Storage.Remove(key);
            }

            private static void Cache<T>(ISupportPaging grid, IEnumerable<T> queryResult)
            {
                var key = new CacheKey(grid);
                if (Storage.ContainsKey(key))
                    Storage.Remove(key);
                Storage.Add(key, queryResult);
            }

            private static IEnumerable<T> GetCached<T>(ISupportPaging grid)
            {
                var key = new CacheKey(grid);
                if (!Storage.ContainsKey(key))
                    return null;
                return (IEnumerable<T>)Storage[key];
            }

            private struct CacheKey
            {
                private readonly ISupportPaging _grid;
                private readonly int _gridPage;

                public CacheKey(ISupportPaging grid)
                {
                    _grid = grid;
                    _gridPage = grid.CurrentPage;
                }

                public override bool Equals(object obj)
                {
                    if (ReferenceEquals(null, obj)) return false;
                    return obj is CacheKey && Equals((CacheKey)obj);
                }

                public override int GetHashCode()
                {
                    unchecked
                    {
                        return ((_grid != null ? _grid.GetHashCode() : 0) * 397) ^ _gridPage;
                    }
                }

                private bool Equals(CacheKey other)
                {
                    return Equals(_grid, other._grid) && _gridPage == other._gridPage;
                }
            }
        }

        #endregion Caching
    }
}